﻿#include "camera.h"
#include "dx_utils.h"

// Inner Fire 游戏引擎库
// 相机类
//
// Copyright (c) 2023 by 尔西大帝. All rights reserved.
//
// 这个文件用于各种相机。根据《DirectX12游戏开发实战》中的工具类修改。
// 原作者：Frank Luna
//
// Author: 尔西大帝
// Email: 2670613871@qq.com
// Created: 2024-11-20

namespace ifire {

SphericalCamera::SphericalCamera() {
  // 设置相机的初始位置
  coord_.theta = 1.5f * XM_PI;
  coord_.phi = XM_PIDIV4;
  coord_.radius = 35.0f;
}

void SphericalCamera::OnMouseMove(WPARAM btn_state, float x, float y) {}

void SphericalCamera::OnZoom(float move_x, float move_y) {
  // 使场景中的每个像素按鼠标移动距离的0.005倍进行缩放
  float dx = 0.05f * move_x;
  float dy = 0.05f * move_y;

  // 更新可视范围
  coord_.radius += dx - dy;

  // 限制可视范围的半径
  coord_.radius = dxutils::clamp(coord_.radius, 5.0f, 150.0f);
}

void SphericalCamera::OnRotate(float move_x, float move_y) {
  // 根据鼠标的移动距离计算旋转角度，并令每个像素都按此角度的1/4旋转
  float dx = XMConvertToRadians(0.25f * move_x);
  float dy = XMConvertToRadians(0.25f * move_y);

  // 更新角色
  coord_.theta += dx;
  coord_.phi += dy;

  // 限制角度的范围
  coord_.phi = dxutils::clamp(coord_.phi, 0.1f, XM_PI - 0.1f);
}

void SphericalCamera::UpdateViewMatrix() {
  // 将球面坐标置换为笛卡尔坐标
  float sin_phi = sinf(coord_.phi);
  float x = coord_.radius * sin_phi * cosf(coord_.theta);
  float z = coord_.radius * sin_phi * sinf(coord_.theta);
  float y = coord_.radius * cosf(coord_.phi);

  // 构建观察矩阵
  XMVECTOR pos = XMVectorSet(x, y, z, 1.0f);
  XMStoreFloat3(&position_, pos); // 将保存矩阵

  XMVECTOR target = XMVectorZero();
  XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);

  view_ = XMMatrixLookAtLH(pos, target, up);
}

Camera::Camera() { SetLens(0.25f * MathHelper::Pi, 1.0f, 1.0f, 1000.0f); }

Camera::~Camera() {}

XMVECTOR Camera::GetPosition() const { return XMLoadFloat3(&mPosition); }

XMFLOAT3 Camera::GetPosition3f() const { return mPosition; }

void Camera::SetPosition(float x, float y, float z) {
  mPosition = XMFLOAT3(x, y, z);
  mViewDirty = true;
}

void Camera::SetPosition(const XMFLOAT3& v) {
  mPosition = v;
  mViewDirty = true;
}

XMVECTOR Camera::GetRight() const { return XMLoadFloat3(&mRight); }

XMFLOAT3 Camera::GetRight3f() const { return mRight; }

XMVECTOR Camera::GetUp() const { return XMLoadFloat3(&mUp); }

XMFLOAT3 Camera::GetUp3f() const { return mUp; }

XMVECTOR Camera::GetLook() const { return XMLoadFloat3(&mLook); }

XMFLOAT3 Camera::GetLook3f() const { return mLook; }

float Camera::GetNearZ() const { return mNearZ; }

float Camera::GetFarZ() const { return mFarZ; }

float Camera::GetAspect() const { return mAspect; }

float Camera::GetFovY() const { return mFovY; }

float Camera::GetFovX() const {
  float halfWidth = 0.5f * GetNearWindowWidth();
  return 2.0f * atan(halfWidth / mNearZ);
}

float Camera::GetNearWindowWidth() const { return mAspect * mNearWindowHeight; }

float Camera::GetNearWindowHeight() const { return mNearWindowHeight; }

float Camera::GetFarWindowWidth() const { return mAspect * mFarWindowHeight; }

float Camera::GetFarWindowHeight() const { return mFarWindowHeight; }

void Camera::SetLens(float fovY, float aspect, float zn, float zf) {
  // cache properties
  mFovY = fovY;
  mAspect = aspect;
  mNearZ = zn;
  mFarZ = zf;

  mNearWindowHeight = 2.0f * mNearZ * tanf(0.5f * mFovY);
  mFarWindowHeight = 2.0f * mFarZ * tanf(0.5f * mFovY);

  XMMATRIX P = XMMatrixPerspectiveFovLH(mFovY, mAspect, mNearZ, mFarZ);
  XMStoreFloat4x4(&mProj, P);
}

void Camera::LookAt(FXMVECTOR pos, FXMVECTOR target, FXMVECTOR worldUp) {
  XMVECTOR L = XMVector3Normalize(XMVectorSubtract(target, pos));
  XMVECTOR R = XMVector3Normalize(XMVector3Cross(worldUp, L));
  XMVECTOR U = XMVector3Cross(L, R);

  XMStoreFloat3(&mPosition, pos);
  XMStoreFloat3(&mLook, L);
  XMStoreFloat3(&mRight, R);
  XMStoreFloat3(&mUp, U);

  mViewDirty = true;
}

void Camera::LookAt(
    const XMFLOAT3& pos, const XMFLOAT3& target, const XMFLOAT3& up) {
  XMVECTOR P = XMLoadFloat3(&pos);
  XMVECTOR T = XMLoadFloat3(&target);
  XMVECTOR U = XMLoadFloat3(&up);

  LookAt(P, T, U);

  mViewDirty = true;
}

XMMATRIX Camera::GetView() const {
  assert(!mViewDirty);
  return XMLoadFloat4x4(&mView);
}

XMMATRIX Camera::GetProj() const { return XMLoadFloat4x4(&mProj); }

XMFLOAT4X4 Camera::GetView4x4f() const {
  assert(!mViewDirty);
  return mView;
}

XMFLOAT4X4 Camera::GetProj4x4f() const { return mProj; }

void Camera::Strafe(float d) {
  // mPosition += d*mRight
  XMVECTOR s = XMVectorReplicate(d);
  XMVECTOR r = XMLoadFloat3(&mRight);
  XMVECTOR p = XMLoadFloat3(&mPosition);
  XMStoreFloat3(&mPosition, XMVectorMultiplyAdd(s, r, p));

  mViewDirty = true;
}

void Camera::Walk(float d) {
  // mPosition += d*mLook
  XMVECTOR s = XMVectorReplicate(d);
  XMVECTOR l = XMLoadFloat3(&mLook);
  XMVECTOR p = XMLoadFloat3(&mPosition);
  XMStoreFloat3(&mPosition, XMVectorMultiplyAdd(s, l, p));

  mViewDirty = true;
}

void Camera::Pitch(float angle) {
  // Rotate up and look vector about the right vector.

  XMMATRIX R = XMMatrixRotationAxis(XMLoadFloat3(&mRight), angle);

  XMStoreFloat3(&mUp, XMVector3TransformNormal(XMLoadFloat3(&mUp), R));
  XMStoreFloat3(&mLook, XMVector3TransformNormal(XMLoadFloat3(&mLook), R));

  mViewDirty = true;
}

void Camera::RotateY(float angle) {
  // Rotate the basis vectors about the world y-axis.

  XMMATRIX R = XMMatrixRotationY(angle);

  XMStoreFloat3(&mRight, XMVector3TransformNormal(XMLoadFloat3(&mRight), R));
  XMStoreFloat3(&mUp, XMVector3TransformNormal(XMLoadFloat3(&mUp), R));
  XMStoreFloat3(&mLook, XMVector3TransformNormal(XMLoadFloat3(&mLook), R));

  mViewDirty = true;
}

void Camera::UpdateViewMatrix() {
  if (mViewDirty) {
    XMVECTOR R = XMLoadFloat3(&mRight);
    XMVECTOR U = XMLoadFloat3(&mUp);
    XMVECTOR L = XMLoadFloat3(&mLook);
    XMVECTOR P = XMLoadFloat3(&mPosition);

    // Keep camera's axes orthogonal to each other and of unit length.
    L = XMVector3Normalize(L);
    U = XMVector3Normalize(XMVector3Cross(L, R));

    // U, L already ortho-normal, so no need to normalize cross product.
    R = XMVector3Cross(U, L);

    // Fill in the view matrix entries.
    float x = -XMVectorGetX(XMVector3Dot(P, R));
    float y = -XMVectorGetX(XMVector3Dot(P, U));
    float z = -XMVectorGetX(XMVector3Dot(P, L));

    XMStoreFloat3(&mRight, R);
    XMStoreFloat3(&mUp, U);
    XMStoreFloat3(&mLook, L);

    mView(0, 0) = mRight.x;
    mView(1, 0) = mRight.y;
    mView(2, 0) = mRight.z;
    mView(3, 0) = x;

    mView(0, 1) = mUp.x;
    mView(1, 1) = mUp.y;
    mView(2, 1) = mUp.z;
    mView(3, 1) = y;

    mView(0, 2) = mLook.x;
    mView(1, 2) = mLook.y;
    mView(2, 2) = mLook.z;
    mView(3, 2) = z;

    mView(0, 3) = 0.0f;
    mView(1, 3) = 0.0f;
    mView(2, 3) = 0.0f;
    mView(3, 3) = 1.0f;

    mViewDirty = false;
  }
}

} // namespace ifire
